package com.medusa.aps.business.modules.basic.mp.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.medusa.aps.business.common.mp.IManualTransaction;
import com.medusa.aps.business.common.redis.RedisUtil;
import com.medusa.aps.business.common.redis.annotation.Redisson;
import com.medusa.aps.business.common.security.resource.helper.ISecurity;
import com.medusa.aps.business.global.model.exception.GlobalException;
import com.medusa.aps.business.model.constant.RedisConstants;
import com.medusa.aps.business.model.pagination.Page;
import com.medusa.aps.business.modules.basic.model.WorkPeriod;
import com.medusa.aps.business.modules.basic.model.input.ClassSystemEquipmentPageParam;
import com.medusa.aps.business.modules.basic.model.input.ClassSystemParam;
import com.medusa.aps.business.modules.basic.model.input.WorkingDayPeriodParam;
import com.medusa.aps.business.modules.basic.model.output.ClassSystemEquipmentModel;
import com.medusa.aps.business.modules.basic.model.output.ClassSystemListModel;
import com.medusa.aps.business.modules.basic.model.output.ClassSystemModel;
import com.medusa.aps.business.modules.basic.model.output.WorkingDayPeriodsModel;
import com.medusa.aps.business.modules.basic.mp.entity.ClassSystem;
import com.medusa.aps.business.modules.basic.mp.entity.WorkingCalendar;
import com.medusa.aps.business.modules.basic.mp.entity.WorkingPeriod;
import com.medusa.aps.business.modules.basic.mp.mapper.ClassSystemMapper;
import com.medusa.aps.business.modules.basic.mp.mapper.WorkingCalendarMapper;
import com.medusa.aps.business.modules.basic.mp.mapper.WorkingPeriodMapper;
import com.medusa.aps.business.modules.basic.mp.service.ClassSystemService;
import com.medusa.aps.business.modules.basic.mp.service.WorkingCalendarService;
import com.medusa.aps.business.modules.modeling.mp.entity.Equipment;
import com.medusa.aps.business.modules.modeling.mp.mapper.EquipmentMapper;
import com.medusa.aps.business.modules.plan.mp.service.ProcessDetailsService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.MonthDay;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 班制服务实现类
 *
 * @author yuxh
 * @since 2023-08-14 17:37:43
 */
@Slf4j
@Service("classSystemService")
public class ClassSystemServiceImpl extends ServiceImpl<ClassSystemMapper, ClassSystem> implements ClassSystemService {

    private static final MonthDay MID_DAY_OF_YEAH = MonthDay.of(7, 1);
    private static final WorkingPeriod REST_PERIOD = new WorkingPeriod().setId(0).setWorkTimeName("休息");
    @Resource
    private EquipmentMapper equipmentMapper;
    @Resource
    private WorkingPeriodMapper workingPeriodMapper;
    @Resource
    private WorkingCalendarMapper workingCalendarMapper;
    @Resource
    private ProcessDetailsService processDetailsService;
    @Resource
    private ClassSystemService classSystemService;
    @Resource
    private WorkingCalendarService workingCalendarService;

    private static String getWeek(int weekday) {
        return switch (weekday) {
            case 1 -> "周一";
            case 2 -> "周二";
            case 3 -> "周三";
            case 4 -> "周四";
            case 5 -> "周五";
            case 6 -> "周六";
            case 7 -> "周日";
            default -> throw new IllegalStateException("Unexpected value: " + weekday);
        };
    }

    @Override
    public IPage<ClassSystemListModel> classPage(Page<ClassSystem> queryPage) {
        //分页查询班制配置
        IPage<ClassSystem> classSystemPage = this.lambdaQuery()
                .orderByDesc(ClassSystem::getCreateTime)
                .page(queryPage);
        //如果为空 直接返回
        if (CollUtil.isEmpty(classSystemPage.getRecords())) {
            return classSystemPage.convert(record -> null);
        }
        //批量查询工作时段信息
        Set<Integer> periodIds = classSystemPage.getRecords()
                .stream()
                .flatMap(record -> record.getPeriodIds().stream())
                .collect(Collectors.toSet());
        Map<Integer, WorkingPeriod> workingPeriodMap = workingPeriodMapper.selectList(
                        Wrappers.lambdaQuery(WorkingPeriod.class)
                                .select(WorkingPeriod::getId, WorkingPeriod::getWorkTimeName, WorkingPeriod::getStartAndEndTimeIntervalSet)
                                .in(WorkingPeriod::getId, periodIds)
                ).stream()
                .collect(Collectors.toMap(WorkingPeriod::getId, v -> v));
        //批量查询设备信息
        Set<Long> equipmentIds = classSystemPage.getRecords()
                .stream()
                .flatMap(record -> record.getDeviceIds().stream())
                .collect(Collectors.toSet());
        Map<Long, Equipment> equipmentMap = equipmentMapper.selectList(
                        Wrappers.lambdaQuery(Equipment.class)
                                .select(Equipment::getId, Equipment::getEquipmentNumber)
                                .in(Equipment::getId, equipmentIds)
                ).stream()
                .collect(Collectors.toMap(Equipment::getId, v -> v));
        //批量查询已生成未来工序集合的设备 id 集合
        Set<Long> existProcessPlanDeviceIds = processDetailsService.getExistProcessPlanEquipmentIds(equipmentIds, LocalDateTime.now());
        //转换为输出模型
        return classSystemPage.convert(
                record -> {
                    Set<Long> currDeviceIds = record.getDeviceIds();
                    return new ClassSystemListModel().setId(record.getId())
                            .setWorkTime(workTime(workingPeriodMap, record.getPeriodIds()))
                            .setUnEditable(CollUtil.containsAny(existProcessPlanDeviceIds, currDeviceIds))
                            .setEquipmentCollection(
                                    currDeviceIds.stream()
                                            .map(equipmentMap::get)
                                            .filter(Objects::nonNull)
                                            .map(equipment -> new ClassSystemEquipmentModel()
                                                    .setId(equipment.getId())
                                                    .setDeviceCode(equipment.getEquipmentNumber())
                                            ).toList()
                            );
                }
        );
    }

    /**
     * 获取班制的工作时段
     * example: 单班制: 09:00-18:00
     * example: 周一、周二、周三、周四、周五单班制: 09:00-18:00，周六双班制: 09:00-17:00
     * example: 周一、周二、周三、周四、周五、周六四班制: 07:30-09:00 09:10-11:00 11:45-14:00 14:10-16:15
     *
     * @param periodMap 工作时段map
     * @param periodIds 工作时段id列表 下标 周一到周日
     * @return 工作时段
     */
    private String workTime(Map<Integer, WorkingPeriod> periodMap, List<Integer> periodIds) {
        //工作时段列表 与工作日列表下标一一对应
        List<WorkingPeriod> periods = periodIds.stream()
                .map(periodId -> {
                    if (periodId == 0) {
                        return REST_PERIOD;
                    }
                    return periodMap.get(periodId);
                })
                .toList();
        StringBuilder workTime = new StringBuilder();
        //是否都是同一个工作时段 如果是同一个工作时段 则直接返回该工作时段
        WorkingPeriod workingPeriodFirst = periods.get(0);
        if (periods.stream().allMatch(period -> period == workingPeriodFirst)) {
            workTime.append(workingPeriodFirst.getWorkTimeName());
            if (workingPeriodFirst.getId() == 0) {
                return workTime.toString();
            }
            workTime.append("：")
                    .append(workingPeriodFirst.getStartAndEndTimeIntervalSet());
            return workTime.toString();
        }
        //否则 不是同一个工作时段 则需要拼接工作时段
        int size = periods.size();
        //根据相同的工作时段分组 id为 0 排在最后
        Map<WorkingPeriod, List<Integer>> periodWorkingDayMap = new TreeMap<>(
                (period1, period2) -> period1.getId() == 0 ? (period2.getId() == 0 ? 0 : 1) : period1.getId() - period2.getId()
        );
        for (int index = 0; index < size; index++) {
            WorkingPeriod workingPeriod = periods.get(index);
            periodWorkingDayMap.computeIfAbsent(workingPeriod, k -> new ArrayList<>()).add(index + 1);
        }
        //拼接工作时段
        periodWorkingDayMap.forEach((workingPeriod, workingDays) -> {
            //工作日 如：周一 周二 周三
            String weekName = workingDays.stream().map(ClassSystemServiceImpl::getWeek).collect(Collectors.joining("、"));
            workTime.append(weekName)
                    .append(workingPeriod.getWorkTimeName())
                    .append(workingPeriod.getId() == 0 ? "" : "：" + workingPeriod.getStartAndEndTimeIntervalSet())
                    .append('，');
        });
        //删除最后一个逗号
        workTime.deleteCharAt(workTime.length() - 1);
        return workTime.toString();
    }

    @Override
    @Redisson(name = RedisConstants.EQUIPMENT_LOCK_KEY, waitTime = 1, leaseTime = 1)
    @Redisson(name = RedisConstants.CLASS_SYSTEM_LOCK_KEY, waitTime = 1)
    public void newClass(ClassSystemParam classSystemParam) {
        classSystemParam.valid();
        Set<Long> equipmentIds = classSystemParam.getEquipmentIds();
        //检查设备是否已经生成了班制
        if (haveUsing(equipmentIds, classSystemParam.getId())) {
            throw new GlobalException("设备已被其他班制绑定，不可重复设置。");
        }
        //按顺序收集 工作日与 工作时段
        List<Integer> periodIds = classSystemParam.periodIds();
        ClassSystem classSystem = new ClassSystem()
                .setPeriodIds(periodIds)
                .setDeviceIds(equipmentIds);
        //批量查询工作时段 并转换为map
        Map<Integer, WorkingPeriod> workingPeriodMap = workingPeriodMapper.selectList(
                        Wrappers.lambdaQuery(WorkingPeriod.class)
                                .select(WorkingPeriod::getId, WorkingPeriod::getWorkTimeName, WorkingPeriod::getStartAndEndTimeIntervalSet)
                                .in(WorkingPeriod::getId, periodIds)
                ).stream()
                .collect(Collectors.toMap(WorkingPeriod::getId, v -> v));
        //判断是否有正在生成的数据
        validSystemClassLock(() -> classSystemService.handlerNewClass(classSystem, classSystemParam, workingPeriodMap));
    }

    @Override
    public void handlerNewClass(ClassSystem classSystem, ClassSystemParam classSystemParam, Map<Integer, WorkingPeriod> workingPeriodMap) {
        try {
            IManualTransaction.todo(() -> {
                this.save(classSystem);
                List<WorkingCalendar> workingCalendars = renderCalendar(classSystem.getId(), LocalDate.now(), classSystemParam, workingPeriodMap);
                List<List<WorkingCalendar>> avg = ListUtil.splitAvg(workingCalendars, 4);
                for (List<WorkingCalendar> calendars : avg) {
                    if (CollUtil.isNotEmpty(calendars)) {
                        workingCalendarMapper.saveBatch(calendars);
                    }
                }
            });
        } catch (GlobalException e) {
            log.error("新增班制失败:", e);
            RedisUtil.setCacheObject(RedisConstants.WORKING_CALENDAR_FAIL_LOCK_KEY, StrUtil.format("时间:{},新增班制失败，请稍后再试", LocalDateTime.now()), 15, TimeUnit.MINUTES);
            throw new GlobalException("新增班制失败");
        }
    }

    @Override
    @Redisson(name = RedisConstants.EQUIPMENT_LOCK_KEY, waitTime = 1)
    @Redisson(name = RedisConstants.CLASS_SYSTEM_LOCK_KEY, waitTime = 1)
    public void updateClass(ClassSystemParam classSystemParam) {
        classSystemParam.valid();
        Integer id = classSystemParam.getId();
        ClassSystem classSystem = this.getById(id);
        if (classSystem == null) {
            throw new GlobalException("班制不存在");
        }
        //校验设备是否已经生成了班制
        Set<Long> equipmentIds = classSystemParam.getEquipmentIds();
        if (haveUsing(equipmentIds, id)) {
            throw new GlobalException("含已被其他班制绑定的设备，不可重复设置。");
        }
        Set<Long> preDeviceIds = classSystem.getDeviceIds();
        Set<Long> allEquipmentIds = new HashSet<>(preDeviceIds);
        allEquipmentIds.addAll(equipmentIds);
        //校验设备是否已生成了工序计划
        if (processDetailsService.isExistProcessPlan(allEquipmentIds, LocalDateTime.now())) {
            throw new GlobalException("包含已生成工序计划的设备，不可修改。");
        }
        //更新班制
        validSystemClassLock(() -> classSystemService.handlerUpdateClass(classSystemParam, classSystem));
    }

    @Override
    public void handlerUpdateClass(ClassSystemParam classSystemParam, ClassSystem classSystem) {
        try {
            IManualTransaction.todo(() -> {
                this.lambdaUpdate()
                        .eq(ClassSystem::getId, classSystemParam.getId())
                        .set(ClassSystem::getPeriodIds, JSON.toJSONString(classSystemParam.periodIds()))
                        .set(ClassSystem::getDeviceIds, JSON.toJSONString(classSystemParam.getEquipmentIds()))
                        .set(ClassSystem::getUpdateTime, LocalDateTime.now())
                        .update();
                //删除工作日历
                //删除班制的工作日历
                if (classSystem.getDeviceIds().size() > 500) {
                    workingCalendarService.delCalendarByLimit(classSystemParam.getId(), LocalDate.now());
                } else {
                    workingCalendarMapper.delete(
                            Wrappers.lambdaUpdate(WorkingCalendar.class)
                                    .eq(WorkingCalendar::getClassId, classSystemParam.getId())
                                    .ge(WorkingCalendar::getDate, LocalDate.now())
                    );
                }
                //批量查询工作时段 并转换为map
                Map<Integer, WorkingPeriod> workingPeriodMap = workingPeriodMapper.selectList(
                                Wrappers.lambdaQuery(WorkingPeriod.class)
                                        .select(WorkingPeriod::getId, WorkingPeriod::getWorkTimeName, WorkingPeriod::getStartAndEndTimeIntervalSet)
                                        .in(WorkingPeriod::getId, classSystemParam.periodIds())
                        ).stream()
                        .collect(Collectors.toMap(WorkingPeriod::getId, v -> v));
                List<WorkingCalendar> workingCalendars = this.renderCalendar(classSystemParam.getId(), LocalDate.now(), classSystemParam, workingPeriodMap);
                List<List<WorkingCalendar>> splitAvg = ListUtil.splitAvg(workingCalendars, 4);
                for (List<WorkingCalendar> calendars : splitAvg) {
                    if (CollUtil.isNotEmpty(calendars)) {
                        workingCalendarMapper.saveBatch(calendars);
                    }
                }
            });
        } catch (Exception e) {
            log.error("更新班制失败:", e);
            RedisUtil.setCacheObject(RedisConstants.WORKING_CALENDAR_FAIL_LOCK_KEY, StrUtil.format("时间：{},更新班制失败,稍后再试", LocalDateTime.now(), 15, TimeUnit.MINUTES));
            throw new GlobalException("更新班制失败");
        }
    }

    /**
     * 检查是否已有班制绑定过该设备
     */
    private boolean haveUsing(Set<Long> equipmentIds, Integer currentClassId) {
        LambdaQueryChainWrapper<ClassSystem> query = this.lambdaQuery();
        if (currentClassId != null) {
            query.ne(ClassSystem::getId, currentClassId);
        }
        query.and(
                innerQw -> equipmentIds.forEach(
                        equipmentId -> innerQw.or().apply("JSON_CONTAINS(device_ids, JSON_ARRAY({0}))", equipmentId)
                )
        );
        return query.count() > 0;
    }

    /**
     * 生成设备工作日历 当前日期到明年最后一天
     *
     * @param currentDate      当前日期
     * @param classSystemParam 班制参数
     * @param workingPeriodMap 工作时段map
     * @return 工作日历
     */
    public List<WorkingCalendar> renderCalendar(Integer classId, LocalDate currentDate, ClassSystemParam classSystemParam, Map<Integer, WorkingPeriod> workingPeriodMap) {
        //获取工作日与工作时段关联参数
        Map<Integer, Integer> workingDayPeriodIdMap = classSystemParam.getWorkingDayPeriods()
                .stream()
                .collect(Collectors.toMap(WorkingDayPeriodParam::getWorkingDay, WorkingDayPeriodParam::getPeriodId));
        //获取结束日期 明年的最后一天
        // 如果当前日期在6月30日之前 则结束日期为今年的最后一天 否则为明年的最后一天
        LocalDate endDate = currentDate.plusYears(
                currentDate.isBefore(currentDate.with(MID_DAY_OF_YEAH)) ? 0 : 1
        ).with(TemporalAdjusters.lastDayOfYear());
        //收集工作日历列表
        Set<Long> equipmentIds = classSystemParam.getEquipmentIds();
        return currentDate
                .datesUntil(endDate.plusDays(1))
                .flatMap(
                        date -> {
                            //获取当前日期的工作日
                            int workingDay = date.getDayOfWeek().getValue();
                            //获取当前日期的工作时段id 没有表示休息
                            Integer periodId = workingDayPeriodIdMap.getOrDefault(workingDay, 0);
                            //获取当前日期的工作时段信息
                            WorkingPeriod workingPeriod = workingPeriodMap.get(periodId);
                            if (periodId != 0 && workingPeriod == null) {
                                throw new GlobalException("工作时段不存在");
                            }
                            //批量新增
                            return equipmentIds.stream()
                                    .map(
                                            equipmentId -> new WorkingCalendar()
                                                    .setClassId(classId)
                                                    .setDeviceId(equipmentId)
                                                    .setDate(date)
                                                    .setWorkingDay(workingDay)
                                                    .setWorkingTimeId(periodId)
                                                    .setPeriodInfo(
                                                            workingPeriod == null ? null :
                                                                    new WorkingCalendar.WorkingTimeInfo()
                                                                            .setName(workingPeriod.getWorkTimeName())
                                                                            .setTimes(WorkPeriod.str2Periods(workingPeriod.getStartAndEndTimeIntervalSet()))
                                                    )
                                    );
                        }
                ).toList();
    }

    @Override
    @Redisson(name = RedisConstants.CLASS_SYSTEM_LOCK_KEY, waitTime = 1)
    public void removeClass(Integer id) {
        //查询班制是否存在
        ClassSystem classSystem = this.getById(id);
        if (classSystem == null) {
            throw new GlobalException("该班制已不存在");
        }
        //查询设备是否已生成工序计划
        if (processDetailsService.isExistProcessPlan(classSystem.getDeviceIds(), LocalDateTime.now())) {
            throw new GlobalException("包含已生成工序计划的设备，不可删除。");
        }
        //删除班制
        validSystemClassLock(() -> classSystemService.handlerDelClassSystem(id, classSystem.getDeviceIds()));
    }

    /**
     * 处理班制时，校验是否有工作日历正在被处理
     */
    private void validSystemClassLock(Runnable runnable) {
        Boolean isSucc = RedisUtil.getRedisTemplate().opsForValue().setIfAbsent(RedisConstants.WORKING_CALENDAR_LOCK_KEY, RedisConstants.WORKING_CALENDAR_LOCK_KEY);
        if (Boolean.FALSE.equals(isSucc)) {
            throw new GlobalException("有班制中的设备正在处理工作日历，请等待处理完之后在进行操作");
        }
        Authentication authentication = ISecurity.getAuthentication(false);
        CompletableFuture.runAsync(() -> ISecurity.withAuthentication(authentication, runnable))
                .whenComplete((a, b) -> RedisUtil.delete(RedisConstants.WORKING_CALENDAR_LOCK_KEY));
    }

    @Override
    public void handlerDelClassSystem(Integer id, Set<Long> deviceIds) {
        try {
            IManualTransaction.todo(() -> {
                //删除班制
                this.removeById(id);
                //删除班制的工作日历
                if (deviceIds.size() > 500) {
                    workingCalendarService.delCalendarByLimit(id, LocalDate.now());
                } else {
                    workingCalendarService.lambdaUpdate()
                            .eq(WorkingCalendar::getClassId, id)
                            .ge(WorkingCalendar::getDate, LocalDate.now())
                            .remove();
                }
            });
        } catch (GlobalException e) {
            log.error("删除班制失败:", e);
            RedisUtil.setCacheObject(RedisConstants.WORKING_CALENDAR_FAIL_LOCK_KEY, StrUtil.format("删除班制失败：{}", LocalDateTime.now()), 15, TimeUnit.MINUTES);
            throw new GlobalException("删除班制失败");
        }
    }

    @Override
    public IPage<ClassSystemEquipmentModel> equipmentPage(ClassSystemEquipmentPageParam queryPage) {
        return baseMapper.equipmentPage(queryPage);
    }

    @Override
    public ClassSystemModel classInfo(Integer id) {
        ClassSystem classSystem = this.getById(id);
        if (classSystem == null) {
            throw new GlobalException("该班制不存在");
        }
        //批量查询工作时段信息
        List<Integer> periodIds = classSystem.getPeriodIds();
        Map<Integer, WorkingPeriod> workingPeriodMap = workingPeriodMapper.selectList(
                        Wrappers.lambdaQuery(WorkingPeriod.class)
                                .select(WorkingPeriod::getId, WorkingPeriod::getWorkTimeName, WorkingPeriod::getStartAndEndTimeIntervalSet)
                                .in(WorkingPeriod::getId, Set.copyOf(periodIds))
                ).stream()
                .collect(Collectors.toMap(WorkingPeriod::getId, v -> v));
        //转换为输出模型
        SortedSet<WorkingDayPeriodsModel> workingDayPeriods = new TreeSet<>();
        for (int index = 0; index < periodIds.size(); index++) {
            Integer periodId = periodIds.get(index);
            WorkingPeriod workingPeriod = periodId == 0 ? null : workingPeriodMap.get(periodId);
            workingDayPeriods.add(
                    new WorkingDayPeriodsModel()
                            .setWorkingDay(index + 1)
                            .setPeriodId(periodId)
                            .setPeriodDesc(workingPeriod == null ? null : workingPeriod.getWorkTimeName() + "：" + workingPeriod.getStartAndEndTimeIntervalSet())
            );
        }
        return new ClassSystemModel()
                .setId(classSystem.getId())
                .setDeviceIds(classSystem.getDeviceIds())
                .setWorkingDayPeriods(workingDayPeriods);
    }

    /**
     * 查询是否有班制正在处理
     *
     * @return 是否有班制正在处理
     */
    @Override
    public Boolean isGenerateWorkingCalendar() {
        //是否有班制正在处理
        Boolean isProgress = RedisUtil.getRedisTemplate().hasKey(RedisConstants.WORKING_CALENDAR_LOCK_KEY);
        if (Boolean.FALSE.equals(isProgress)) {
            //是否执行失败
            Object localMessage = RedisUtil.getCacheObject(RedisConstants.WORKING_CALENDAR_FAIL_LOCK_KEY);
            if (localMessage != null) {
                RedisUtil.delete(RedisConstants.WORKING_CALENDAR_FAIL_LOCK_KEY);
                throw new GlobalException(localMessage + "");
            }
        }
        return isProgress;
    }
}

